﻿using Tesla.Content;
using Tesla.Core;
using Tesla.Graphics;
using Tesla.Math;
using Tesla.Scene;
using Tesla.Scene.Shape;

namespace TeslaSamples.Renderer {
    [AppDescription("Hardware Instancing", PlatformCapabilities.Direct3D10 | PlatformCapabilities.XNA4, "InstancingImage", "InstancingDesc", "InstancingSource")]
    public class InstancingSample : BasicApp {
        private Mesh mesh;
        private Sphere sphere;
        private int numInstances = 256;
        private VertexBuffer worldTransforms;
        private Matrix[] transformArray;
        private Material material;
        private VertexBufferBinding[] buffers;
        private float acctime = 0;

        protected override void LoadContent() {
            Window.Title = "Instancing Sample";
            ClearColor = Color.Black;

            //Let's create a mesh that will serve as our data. We're technically not using the scene graph here, this is just a convience.
            //Maybe at some point the engine's scene graph will have a mesh structure that supports instancing, for now we have to do it ourselves.
            mesh = new Box("Box", 2, 2, 5);
            mesh.SetRandomColors();

            //We use the default lights from the root node, so all our instances will be lit.
            mesh.Lights.Set(RootNode.Lights);
            mesh.UpdateWorldLights(false);

            //Create a target object that all our instances will "look at"
            sphere = new Sphere("Sphere", 30, 30, 10);
            sphere.Material = ContentManager.Load<Material>("LitBasicColor.tem").Clone();
            sphere.Material.SetParameter("MatDiffuse", Color.Yellow.ToVector3());
            RootNode.AddChild(sphere);

            // The mesh will have a vertex buffer that goes something like so:
            //
            //Position0 - Vector3
            //Normal0 - Vector3
            //Color0 - Color
            //TexCoord0 - Vector2
            //
            // Meanwhile, our second vertex buffer that contains the world transforms will look like this, since we're passing a 4x4 matrix:
            //
            //TexCoord0 - Vector4
            //TexCoord1 - Vector4
            //TexCoord2 - Vector4
            //TexCoord3 - Vector4
            //
            // When we set these two buffers at the same time (first to slot 0, second to slot 1), the engine automatically
            //merges the declarations and resolves conflicts such as duplicate semantic indices - in the order the
            //vertex buffers are set to the device, so in this example the semantic indices of the second buffer are all incremented
            //by one.

            VertexDeclaration decl = new VertexDeclaration(new VertexElement[] {
                new VertexElement(VertexSemantic.TextureCoordinate, 0, VertexFormat.Vector4, 0),
                new VertexElement(VertexSemantic.TextureCoordinate, 1, VertexFormat.Vector4, 16),
                new VertexElement(VertexSemantic.TextureCoordinate, 2, VertexFormat.Vector4, 32),
                new VertexElement(VertexSemantic.TextureCoordinate, 3, VertexFormat.Vector4, 48)
            });

            //We want a dynamic vertex buffer for the world transforms since we'll be updating each instance's
            //rotation to "look at" the sphere.
            worldTransforms = new VertexBuffer(decl, numInstances, ResourceUsage.Dynamic);
            transformArray = new Matrix[numInstances];
            int index = 0;
            //Set the instances into a 2D grid on the XY plane.
            for (int x = 0; x < 16; x++) {
                for (int y = 0; y < 16; y++) {
                    Matrix transform = Matrix.FromTranslation(new Vector3((x - 7.5f) * 15.0f, (y - 7.5f) * 15.0f, 0.0f));
                    transformArray[index++] = transform;
                }
           }

            worldTransforms.SetData<Matrix>(transformArray);

            //Setup our vertex buffer bindings - the world transforms are set to an instance frequency of one, so for each
            //instance we'll use one "vertex" from that buffer (which is the world transform)
            buffers = new VertexBufferBinding[] { mesh.MeshData.VertexBuffer, new VertexBufferBinding(worldTransforms, 0, 1) };

            //Load a material we've created
            material = ContentManager.Load<Material>("Materials//InstancedMaterial.tem");
            material.SetParameter("DiffuseMap", ContentManager.Load<Texture>("Textures//rock_diff.dds"));
        }

        protected override void OnUpdate(GameTime time) {
            acctime += (float) Engine.ValueMap.TimePerFrame;
            sphere.Translation = new Vector3((float)System.Math.Sin(acctime) * 70, (float)System.Math.Cos(acctime) * 70, (float)System.Math.Cos(acctime) * 40);

            for(int i = 0; i < transformArray.Length; i++) {
                Matrix transform = transformArray[i];
                Vector3 pos = transform.Translation;
                transformArray[i] = Matrix.LookAt(pos, sphere.Translation, Vector3.Up) * Matrix.FromTranslation(pos);
            }

            worldTransforms.SetData<Matrix>(transformArray, DataWriteOptions.Discard);
        }

        protected override void OnRenderPost(IRenderer renderer) {
            Engine.ValueMap.WorldMatrix = Matrix.Identity;

            material.ApplyMaterial(renderer, mesh);

            material.ApplyPass(renderer, 0);

            renderer.RasterizerState = RasterizerState.CullBackClockwiseFront;
            MeshData md = mesh.MeshData;
            renderer.SetVertexBuffers(buffers);
            renderer.SetIndexBuffer(md.IndexBuffer);
            renderer.DrawIndexedInstanced(md.PrimitiveType, md.IndexCount, 0, 0, numInstances);
        }
    }
}
